<?php

namespace Luracast\Restler;

use Luracast\Restler\Data\String;
use Luracast\Restler\Scope;
use stdClass;

/**
 * API Class to create Swagger Spec 1.1 compatible id and operation
 * listing
 *
 * @category Framework
 * @package Restler
 * @author R.Arul Kumaran <arul@luracast.com>
 * @copyright 2010 Luracast
 * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @link http://luracast.com/products/restler/
 * @version 3.0.0rc5
 */
class Resources implements iUseAuthentication, iProvideMultiVersionApi {
	/**
	 *
	 * @var bool should protected resources be shown to unauthenticated users?
	 */
	public static $hideProtected = true;
	/**
	 *
	 * @var bool should we use format as extension?
	 */
	public static $useFormatAsExtension = true;
	/**
	 *
	 * @var bool should we include newer apis in the list? works only when
	 *      Defaults::$useUrlBasedVersioning is set to true;
	 */
	public static $listHigherVersions = true;
	/**
	 *
	 * @var array all http methods specified here will be excluded from
	 *      documentation
	 */
	public static $excludedHttpMethods = array (
			'OPTIONS' 
	);
	/**
	 *
	 * @var array all paths beginning with any of the following will be excluded
	 *      from documentation
	 */
	public static $excludedPaths = array ();
	/**
	 *
	 * @var bool
	 */
	public static $placeFormatExtensionBeforeDynamicParts = true;
	/**
	 *
	 * @var bool should we group all the operations with the same url or not
	 */
	public static $groupOperations = false;
	/**
	 *
	 * @var null|callable if the api methods are under access control mechanism
	 *      you can attach a function here that returns true or false to determine
	 *      visibility of a protected api method. this function will receive method
	 *      info as the only parameter.
	 */
	public static $accessControlFunction = null;
	/**
	 *
	 * @var array type mapping for converting data types to javascript / swagger
	 */
	public static $dataTypeAlias = array (
			'string' => 'string',
			'int' => 'int',
			'number' => 'float',
			'float' => 'float',
			'bool' => 'boolean',
			'boolean' => 'boolean',
			'NULL' => 'null',
			'array' => 'Array',
			'object' => 'Object',
			'stdClass' => 'Object',
			'mixed' => 'string',
			'DateTime' => 'Date' 
	);
	/**
	 *
	 * @var array configurable symbols to differentiate public, hybrid and
	 *      protected api
	 */
	public static $apiDescriptionSuffixSymbols = array (
			0 => '&nbsp; <i class="icon-unlock-alt icon-large"></i>', // public api
			1 => '&nbsp; <i class="icon-adjust icon-large"></i>', // hybrid api
			2 => '&nbsp; <i class="icon-lock icon-large"></i>' 
	) // protected api
;
	
	/**
	 * Injected at runtime
	 *
	 * @var Restler instance of restler
	 */
	public $restler;
	/**
	 *
	 * @var string when format is not used as the extension this property is
	 *      used to set the extension manually
	 */
	public $formatString = '';
	protected $_models;
	protected $_bodyParam;
	/**
	 *
	 * @var bool|stdClass
	 */
	protected $_fullDataRequested = false;
	protected $crud = array (
			'POST' => 'create',
			'GET' => 'retrieve',
			'PUT' => 'update',
			'DELETE' => 'delete',
			'PATCH' => 'partial update' 
	);
	protected static $prefixes = array (
			'get' => 'retrieve',
			'index' => 'list',
			'post' => 'create',
			'put' => 'update',
			'patch' => 'modify',
			'delete' => 'remove' 
	);
	protected $_authenticated = false;
	protected $cacheName = '';
	public function __construct() {
		if (static::$useFormatAsExtension) {
			$this->formatString = '.{format}';
		}
	}
	
	/**
	 * This method will be called first for filter classes and api classes so
	 * that they can respond accordingly for filer method call and api method
	 * calls
	 *
	 *
	 * @param bool $isAuthenticated
	 *        	passes true when the authentication is
	 *        	done, false otherwise
	 *        	
	 * @return mixed
	 */
	public function __setAuthenticationStatus($isAuthenticated = false) {
		$this->_authenticated = $isAuthenticated;
	}
	
	/**
	 * pre call for get($id)
	 *
	 * if cache is present, use cache
	 */
	public function _pre_get_json($id) {
		$userClass = Defaults::$userIdentifierClass;
		$this->cacheName = $userClass::getCacheIdentifier () . '_resources_' . $id;
		if ($this->restler->getProductionMode () && ! $this->restler->refreshCache && $this->restler->cache->isCached ( $this->cacheName )) {
			// by pass call, compose, postCall stages and directly send response
			$this->restler->composeHeaders ();
			die ( $this->restler->cache->get ( $this->cacheName ) );
		}
	}
	
	/**
	 * post call for get($id)
	 *
	 * create cache if in production mode
	 *
	 * @param
	 *        	$responseData
	 *        	
	 * @internal param string $data composed json output
	 *          
	 * @return string
	 */
	public function _post_get_json($responseData) {
		if ($this->restler->getProductionMode ()) {
			$this->restler->cache->set ( $this->cacheName, $responseData );
		}
		return $responseData;
	}
	
	/**
	 *
	 * @access hybrid
	 *        
	 * @param string $id        	
	 *
	 * @throws RestException
	 * @return null|stdClass @url GET {id}
	 */
	public function get($id = '') {
		$version = $this->restler->getRequestedApiVersion ();
		if (empty ( $id )) {
			// do nothing
		} elseif (false !== ($pos = strpos ( $id, '-v' ))) {
			// $version = intval(substr($id, $pos + 2));
			$id = substr ( $id, 0, $pos );
		} elseif ($id {0} == 'v' && is_numeric ( $v = substr ( $id, 1 ) )) {
			$id = '';
			// $version = $v;
		} elseif ($id == 'root' || $id == 'index') {
			$id = '';
		}
		$this->_models = new stdClass ();
		$r = null;
		$count = 0;
		
		$tSlash = ! empty ( $id );
		$target = empty ( $id ) ? '' : $id;
		$tLen = strlen ( $target );
		
		$filter = array ();
		
		$routes = Util::nestedValue ( Routes::toArray (), "v$version" ) ?  : array ();
		
		$prefix = Defaults::$useUrlBasedVersioning ? "/v$version" : '';
		
		foreach ( $routes as $value ) {
			foreach ( $value as $httpMethod => $route ) {
				if (in_array ( $httpMethod, static::$excludedHttpMethods )) {
					continue;
				}
				$fullPath = $route ['url'];
				if ($fullPath !== $target && ! String::beginsWith ( $fullPath, $target )) {
					continue;
				}
				$fLen = strlen ( $fullPath );
				if ($tSlash) {
					if ($fLen != $tLen && ! String::beginsWith ( $fullPath, $target . '/' ))
						continue;
				} elseif ($fLen > $tLen + 1 && $fullPath {$tLen + 1} != '{' && ! String::beginsWith ( $fullPath, '{' )) {
					// when mapped to root exclude paths that have static parts
					// they are listed else where under that static part name
					continue;
				}
				
				if (! static::verifyAccess ( $route )) {
					continue;
				}
				foreach ( static::$excludedPaths as $exclude ) {
					if (empty ( $exclude )) {
						if ($fullPath == $exclude)
							continue 2;
					} elseif (String::beginsWith ( $fullPath, $exclude )) {
						continue 2;
					}
				}
				$m = $route ['metadata'];
				if ($id == '' && $m ['resourcePath'] != '') {
					continue;
				}
				if (isset ( $filter [$httpMethod] [$fullPath] )) {
					continue;
				}
				$filter [$httpMethod] [$fullPath] = true;
				// reset body params
				$this->_bodyParam = array (
						'required' => false,
						'description' => array () 
				);
				$count ++;
				$className = $this->_noNamespace ( $route ['className'] );
				if (! $r) {
					$resourcePath = '/' . trim ( $m ['resourcePath'], '/' );
					$r = $this->_operationListing ( $resourcePath );
				}
				$parts = explode ( '/', $fullPath );
				$pos = count ( $parts ) - 1;
				if (count ( $parts ) == 1 && $httpMethod == 'GET') {
				} else {
					for($i = 0; $i < count ( $parts ); $i ++) {
						if (strlen ( $parts [$i] ) && $parts [$i] {0} == '{') {
							$pos = $i - 1;
							break;
						}
					}
				}
				$nickname = $this->_nickname ( $route );
				$index = static::$placeFormatExtensionBeforeDynamicParts && $pos > 0 ? $pos : 0;
				if (! empty ( $parts [$index] ))
					$parts [$index] .= $this->formatString;
				
				$fullPath = implode ( '/', $parts );
				$description = isset ( $m ['classDescription'] ) ? $m ['classDescription'] : $className . ' API';
				if (empty ( $m ['description'] )) {
					$m ['description'] = $this->restler->getProductionMode () ? '' : 'routes to <mark>' . $route ['className'] . '::' . $route ['methodName'] . '();</mark>';
				}
				if (empty ( $m ['longDescription'] )) {
					$m ['longDescription'] = $this->restler->getProductionMode () ? '' : 'Add PHPDoc long description to ' . "<mark>$className::" . $route ['methodName'] . '();</mark>' . '  (the api method) to write here';
				}
				$operation = $this->_operation ( $route, $nickname, $httpMethod, $m ['description'], $m ['longDescription'] );
				if (isset ( $m ['throws'] )) {
					foreach ( $m ['throws'] as $exception ) {
						$operation->errorResponses [] = array (
								'reason' => $exception ['reason'],
								'code' => $exception ['code'] 
						);
					}
				}
				if (isset ( $m ['param'] )) {
					foreach ( $m ['param'] as $param ) {
						// combine body params as one
						$p = $this->_parameter ( $param );
						if ($p->paramType == 'body') {
							$this->_appendToBody ( $p );
						} else {
							$operation->parameters [] = $p;
						}
					}
				}
				if (count ( $this->_bodyParam ['description'] ) || ($this->_fullDataRequested && $httpMethod != 'GET' && $httpMethod != 'DELETE')) {
					$operation->parameters [] = $this->_getBody ();
				}
				if (isset ( $m ['return'] ['type'] )) {
					$responseClass = $m ['return'] ['type'];
					if (is_string ( $responseClass )) {
						if (class_exists ( $responseClass )) {
							$this->_model ( $responseClass );
							$operation->responseClass = $this->_noNamespace ( $responseClass );
						} elseif (strtolower ( $responseClass ) == 'array') {
							$operation->responseClass = 'Array';
							$rt = $m ['return'];
							if (isset ( $rt [CommentParser::$embeddedDataName] ['type'] )) {
								$rt = $rt [CommentParser::$embeddedDataName] ['type'];
								if (class_exists ( $rt )) {
									$this->_model ( $rt );
									$operation->responseClass .= '[' . $this->_noNamespace ( $rt ) . ']';
								}
							}
						}
					}
				}
				$api = false;
				
				if (static::$groupOperations) {
					foreach ( $r->apis as $a ) {
						if ($a->path == "/$fullPath") {
							$api = $a;
							break;
						}
					}
				}
				
				if (! $api) {
					$api = $this->_api ( "$prefix/$fullPath", $description );
					$r->apis [] = $api;
				}
				
				$api->operations [] = $operation;
			}
		}
		if (! $count) {
			throw new RestException ( 404 );
		}
		if (! is_null ( $r ))
			$r->models = $this->_models;
		usort ( $r->apis, function ($a, $b) {
			$order = array (
					'GET' => 1,
					'POST' => 2,
					'PUT' => 3,
					'PATCH' => 4,
					'DELETE' => 5 
			);
			return $a->operations [0]->httpMethod == $b->operations [0]->httpMethod ? $a->path > $b->path : $order [$a->operations [0]->httpMethod] > $order [$b->operations [0]->httpMethod];
		} );
		return $r;
	}
	protected function _nickname(array $route) {
		static $hash = array ();
		$method = $route ['methodName'];
		if (isset ( static::$prefixes [$method] )) {
			$method = static::$prefixes [$method];
		} else {
			$method = str_replace ( array_keys ( static::$prefixes ), array_values ( static::$prefixes ), $method );
		}
		while ( isset ( $hash [$method] ) && $route ['url'] != $hash [$method] ) {
			// create another one
			$method .= '_';
		}
		$hash [$method] = $route ['url'];
		return $method;
	}
	protected function _noNamespace($className) {
		$className = explode ( '\\', $className );
		return end ( $className );
	}
	protected function _operationListing($resourcePath = '/') {
		$r = $this->_resourceListing ();
		$r->resourcePath = $resourcePath;
		$r->models = new stdClass ();
		return $r;
	}
	protected function _resourceListing() {
		$r = new stdClass ();
		$r->apiVersion = ( string ) $this->restler->_requestedApiVersion;
		$r->swaggerVersion = "1.1";
		$r->basePath = $this->restler->getBaseUrl ();
		$r->produces = $this->restler->getWritableMimeTypes ();
		$r->consumes = $this->restler->getReadableMimeTypes ();
		$r->apis = array ();
		return $r;
	}
	protected function _api($path, $description = '') {
		$r = new stdClass ();
		$r->path = $path;
		$r->description = empty ( $description ) && $this->restler->getProductionMode () ? 'Use PHPDoc comment to describe here' : $description;
		$r->operations = array ();
		return $r;
	}
	protected function _operation($route, $nickname, $httpMethod = 'GET', $summary = 'description', $notes = 'long description', $responseClass = 'void') {
		// reset body params
		$this->_bodyParam = array (
				'required' => false,
				'description' => array () 
		);
		
		$r = new stdClass ();
		$r->httpMethod = $httpMethod;
		$r->nickname = $nickname;
		$r->responseClass = $responseClass;
		
		$r->parameters = array ();
		
		$r->summary = $summary . ($route ['accessLevel'] > 2 ? static::$apiDescriptionSuffixSymbols [2] : static::$apiDescriptionSuffixSymbols [$route ['accessLevel']]);
		$r->notes = $notes;
		
		$r->errorResponses = array ();
		return $r;
	}
	protected function _parameter($param) {
		$r = new stdClass ();
		$r->name = $param ['name'];
		$r->description = ! empty ( $param ['description'] ) ? $param ['description'] . '.' : ($this->restler->getProductionMode () ? '' : 'add <mark>@param {type} $' . $r->name . ' {comment}</mark> to describe here');
		// paramType can be path or query or body or header
		$r->paramType = Util::nestedValue ( $param, CommentParser::$embeddedDataName, 'from' ) ?  : 'query';
		$r->required = isset ( $param ['required'] ) && $param ['required'];
		if (isset ( $param ['default'] )) {
			$r->defaultValue = $param ['default'];
		} elseif (isset ( $param [CommentParser::$embeddedDataName] ['example'] )) {
			$r->defaultValue = $param [CommentParser::$embeddedDataName] ['example'];
		}
		$r->allowMultiple = false;
		$type = 'string';
		if (isset ( $param ['type'] )) {
			$type = $param ['type'];
			if (is_array ( $type )) {
				$type = array_shift ( $type );
			}
			if ($type == 'array') {
				$contentType = Util::nestedValue ( $param, CommentParser::$embeddedDataName, 'type' );
				if ($contentType) {
					if ($contentType == 'indexed') {
						$type = 'Array';
					} elseif ($contentType == 'associative') {
						$type = 'Object';
					} else {
						$type = "Array[$contentType]";
					}
					if (Util::isObjectOrArray ( $contentType )) {
						$this->_model ( $contentType );
					}
				} elseif (isset ( static::$dataTypeAlias [$type] )) {
					$type = static::$dataTypeAlias [$type];
				}
			} elseif (Util::isObjectOrArray ( $type )) {
				$this->_model ( $type );
			} elseif (isset ( static::$dataTypeAlias [$type] )) {
				$type = static::$dataTypeAlias [$type];
			}
		}
		$r->dataType = $type;
		if (isset ( $param [CommentParser::$embeddedDataName] )) {
			$p = $param [CommentParser::$embeddedDataName];
			if (isset ( $p ['min'] ) && isset ( $p ['max'] )) {
				$r->allowableValues = array (
						'valueType' => 'RANGE',
						'min' => $p ['min'],
						'max' => $p ['max'] 
				);
			} elseif (isset ( $p ['choice'] )) {
				$r->allowableValues = array (
						'valueType' => 'LIST',
						'values' => $p ['choice'] 
				);
			}
		}
		return $r;
	}
	protected function _appendToBody($p) {
		if ($p->name === Defaults::$fullRequestDataName) {
			$this->_fullDataRequested = $p;
			unset ( $this->_bodyParam ['names'] [Defaults::$fullRequestDataName] );
			return;
		}
		$this->_bodyParam ['description'] [$p->name] = "$p->name" . ' : <tag>' . $p->dataType . '</tag> ' . ($p->required ? ' <i>(required)</i> - ' : ' - ') . $p->description;
		$this->_bodyParam ['required'] = $p->required || $this->_bodyParam ['required'];
		$this->_bodyParam ['names'] [$p->name] = $p;
	}
	protected function _getBody() {
		$r = new stdClass ();
		$n = isset ( $this->_bodyParam ['names'] ) ? array_values ( $this->_bodyParam ['names'] ) : array ();
		if (count ( $n ) == 1) {
			if (isset ( $this->_models->{$n [0]->dataType} )) {
				// ============ custom class ===================
				$r = $n [0];
				$c = $this->_models->{$r->dataType};
				$a = $c->properties;
				$r->description = "Paste JSON data here";
				if (count ( $a )) {
					$r->description .= " with the following" . (count ( $a ) > 1 ? ' properties.' : ' property.');
					foreach ( $a as $k => $v ) {
						$r->description .= "<hr/>$k : <tag>" . $v ['type'] . '</tag> ' . (isset ( $v ['required'] ) ? '(required)' : '') . ' - ' . $v ['description'];
					}
				}
				$r->defaultValue = "{\n    \"" . implode ( "\": \"\",\n    \"", array_keys ( $c->properties ) ) . "\": \"\"\n}";
				return $r;
			} elseif (false !== ($p = strpos ( $n [0]->dataType, '[' ))) {
				// ============ array of custom class ===============
				$r = $n [0];
				$t = substr ( $r->dataType, $p + 1, - 1 );
				if ($c = Util::nestedValue ( $this->_models, $t )) {
					$a = $c->properties;
					$r->description = "Paste JSON data here";
					if (count ( $a )) {
						$r->description .= " with an array of objects with the following" . (count ( $a ) > 1 ? ' properties.' : ' property.');
						foreach ( $a as $k => $v ) {
							$r->description .= "<hr/>$k : <tag>" . $v ['type'] . '</tag> ' . (isset ( $v ['required'] ) ? '(required)' : '') . ' - ' . $v ['description'];
						}
					}
					$r->defaultValue = "[\n    {\n        \"" . implode ( "\": \"\",\n        \"", array_keys ( $c->properties ) ) . "\": \"\"\n    }\n]";
					return $r;
				} else {
					$r->description = "Paste JSON data here with an array of $t values.";
					$r->defaultValue = "[ ]";
					return $r;
				}
			} elseif ($n [0]->dataType == 'Array') {
				// ============ array ===============================
				$r = $n [0];
				$r->description = "Paste JSON array data here" . ($r->required ? ' (required) . ' : '. ') . "<br/>$r->description";
				$r->defaultValue = "[\n    {\n        \"" . "property\" : \"\"\n    }\n]";
				return $r;
			} elseif ($n [0]->dataType == 'Object') {
				// ============ object ==============================
				$r = $n [0];
				$r->description = "Paste JSON object data here" . ($r->required ? ' (required) . ' : '. ') . "<br/>$r->description";
				$r->defaultValue = "{\n    \"" . "property\" : \"\"\n}";
				return $r;
			}
		}
		$p = array_values ( $this->_bodyParam ['description'] );
		$r->name = 'REQUEST_BODY';
		$r->description = "Paste JSON data here";
		if (count ( $p ) == 0 && $this->_fullDataRequested) {
			$r->required = $this->_fullDataRequested->required;
			$r->defaultValue = "{\n    \"property\" : \"\"\n}";
		} else {
			$r->description .= " with the following" . (count ( $p ) > 1 ? ' properties.' : ' property.') . '<hr/>' . implode ( "<hr/>", $p );
			$r->required = $this->_bodyParam ['required'];
			// Create default object that includes parameters to be submitted
			$defaultObject = new \StdClass ();
			foreach ( $this->_bodyParam ['names'] as $name => $values ) {
				if (! $values->required)
					continue;
				if (class_exists ( $values->dataType )) {
					$myClassName = $values->dataType;
					$defaultObject->$name = new $myClassName ();
				} else {
					$defaultObject->$name = '';
				}
			}
			$r->defaultValue = Scope::get ( 'JsonFormat' )->encode ( $defaultObject, true );
		}
		$r->paramType = 'body';
		$r->allowMultiple = false;
		$r->dataType = 'Object';
		return $r;
	}
	protected function _model($className, $instance = null) {
		$id = $this->_noNamespace ( $className );
		if (isset ( $this->_models->{$id} )) {
			return;
		}
		$properties = array ();
		if (! $instance) {
			if (! class_exists ( $className ))
				return;
			$instance = new $className ();
		}
		$data = get_object_vars ( $instance );
		$reflectionClass = new \ReflectionClass ( $className );
		foreach ( $data as $key => $value ) {
			
			$propertyMetaData = null;
			
			try {
				$property = $reflectionClass->getProperty ( $key );
				if ($c = $property->getDocComment ()) {
					$propertyMetaData = Util::nestedValue ( CommentParser::parse ( $c ), 'var' );
				}
			} catch ( \ReflectionException $e ) {
			}
			
			if (is_null ( $propertyMetaData )) {
				$type = $this->getType ( $value, true );
				$description = '';
			} else {
				$type = Util::nestedValue ( $propertyMetaData, 'type' ) ?  : $this->getType ( $value, true );
				$description = Util::nestedValue ( $propertyMetaData, 'description' ) ?  : '';
				
				if (class_exists ( $type )) {
					$this->_model ( $type );
					$type = $this->_noNamespace ( $type );
				}
			}
			
			if (isset ( static::$dataTypeAlias [$type] )) {
				$type = static::$dataTypeAlias [$type];
			}
			$properties [$key] = array (
					'type' => $type,
					'description' => $description 
			);
			if (Util::nestedValue ( $propertyMetaData, CommentParser::$embeddedDataName, 'required' )) {
				$properties [$key] ['required'] = true;
			}
			if ($type == 'Array') {
				$itemType = Util::nestedValue ( $propertyMetaData, CommentParser::$embeddedDataName, 'type' ) ?  : (count ( $value ) ? $this->getType ( end ( $value ), true ) : 'string');
				if (class_exists ( $itemType )) {
					$this->_model ( $itemType );
					$itemType = $this->_noNamespace ( $itemType );
				}
				$properties [$key] ['item'] = array (
						'type' => $itemType,
                    /*'description' => '' */ //TODO: add description
                );
			} else if (preg_match ( '/^Array\[(.+)\]$/', $type, $matches )) {
				$itemType = $matches [1];
				$properties [$key] ['type'] = 'Array';
				$properties [$key] ['item'] ['type'] = $itemType;
				
				if (class_exists ( $itemType )) {
					$this->_model ( $itemType );
				}
			}
		}
		if (! empty ( $properties )) {
			$model = new stdClass ();
			$model->id = $id;
			$model->properties = $properties;
			$this->_models->{$id} = $model;
		}
	}
	
	/**
	 * Find the data type of the given value.
	 *
	 *
	 * @param mixed $o
	 *        	given value for finding type
	 *        	
	 * @param bool $appendToModels
	 *        	if an object is found should we append to
	 *        	our models list?
	 *        	
	 * @return string
	 *
	 * @access private
	 */
	public function getType($o, $appendToModels = false) {
		if (is_object ( $o )) {
			$oc = get_class ( $o );
			if ($appendToModels) {
				$this->_model ( $oc, $o );
			}
			return $this->_noNamespace ( $oc );
		}
		if (is_array ( $o )) {
			if (count ( $o )) {
				$child = end ( $o );
				if (Util::isObjectOrArray ( $child )) {
					$childType = $this->getType ( $child, $appendToModels );
					return "Array[$childType]";
				}
			}
			return 'array';
		}
		if (is_bool ( $o ))
			return 'boolean';
		if (is_numeric ( $o ))
			return is_float ( $o ) ? 'float' : 'int';
		return 'string';
	}
	
	/**
	 * pre call for index()
	 *
	 * if cache is present, use cache
	 */
	public function _pre_index_json() {
		$userClass = Defaults::$userIdentifierClass;
		$this->cacheName = $userClass::getCacheIdentifier () . '_resources-v' . $this->restler->_requestedApiVersion;
		if ($this->restler->getProductionMode () && ! $this->restler->refreshCache && $this->restler->cache->isCached ( $this->cacheName )) {
			// by pass call, compose, postCall stages and directly send response
			$this->restler->composeHeaders ();
			die ( $this->restler->cache->get ( $this->cacheName ) );
		}
	}
	
	/**
	 * post call for index()
	 *
	 * create cache if in production mode
	 *
	 * @param
	 *        	$responseData
	 *        	
	 * @internal param string $data composed json output
	 *          
	 * @return string
	 */
	public function _post_index_json($responseData) {
		if ($this->restler->getProductionMode ()) {
			$this->restler->cache->set ( $this->cacheName, $responseData );
		}
		return $responseData;
	}
	
	/**
	 *
	 * @access hybrid
	 * @return \stdClass
	 */
	public function index() {
		if (! static::$accessControlFunction && Defaults::$accessControlFunction)
			static::$accessControlFunction = Defaults::$accessControlFunction;
		$version = $this->restler->getRequestedApiVersion ();
		$allRoutes = Util::nestedValue ( Routes::toArray (), "v$version" );
		$r = $this->_resourceListing ();
		$map = array ();
		if (isset ( $allRoutes ['*'] )) {
			$this->_mapResources ( $allRoutes ['*'], $map, $version );
			unset ( $allRoutes ['*'] );
		}
		$this->_mapResources ( $allRoutes, $map, $version );
		foreach ( $map as $path => $description ) {
			if (! String::contains ( $path, '{' )) {
				// add id
				$r->apis [] = array (
						'path' => $path . $this->formatString,
						'description' => $description 
				);
			}
		}
		if (Defaults::$useUrlBasedVersioning && static::$listHigherVersions) {
			$nextVersion = $version + 1;
			if ($nextVersion <= $this->restler->getApiVersion ()) {
				list ( $status, $data ) = $this->_loadResource ( "/v$nextVersion/resources.json" );
				if ($status == 200) {
					$r->apis = array_merge ( $r->apis, $data->apis );
					$r->apiVersion = $data->apiVersion;
				}
			}
		}
		return $r;
	}
	protected function _loadResource($url) {
		$ch = curl_init ( $this->restler->getBaseUrl () . $url . (empty ( $_GET ) ? '' : '?' . http_build_query ( $_GET )) );
		curl_setopt ( $ch, CURLOPT_CONNECTTIMEOUT, 5 );
		curl_setopt ( $ch, CURLOPT_TIMEOUT, 15 );
		curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, true );
		curl_setopt ( $ch, CURLOPT_HTTPHEADER, array (
				'Accept:application/json' 
		) );
		$result = json_decode ( curl_exec ( $ch ) );
		$http_status = ( int ) curl_getinfo ( $ch, CURLINFO_HTTP_CODE );
		return array (
				$http_status,
				$result 
		);
	}
	protected function _mapResources(array $allRoutes, array &$map, $version = 1) {
		foreach ( $allRoutes as $fullPath => $routes ) {
			$path = explode ( '/', $fullPath );
			$resource = isset ( $path [0] ) ? $path [0] : '';
			if ($resource == 'resources' || String::endsWith ( $resource, 'index' ))
				continue;
			foreach ( $routes as $httpMethod => $route ) {
				if (in_array ( $httpMethod, static::$excludedHttpMethods )) {
					continue;
				}
				if (! static::verifyAccess ( $route )) {
					continue;
				}
				
				foreach ( static::$excludedPaths as $exclude ) {
					if (empty ( $exclude )) {
						if ($fullPath == $exclude)
							continue 2;
					} elseif (String::beginsWith ( $fullPath, $exclude )) {
						continue 2;
					}
				}
				
				$res = $resource ? ($version == 1 ? "/resources/$resource" : "/v$version/resources/$resource-v$version") : ($version == 1 ? "/resources/root" : "/v$version/resources/root-v$version");
				
				if (empty ( $map [$res] )) {
					$map [$res] = isset ( $route ['metadata'] ['classDescription'] ) ? $route ['metadata'] ['classDescription'] : '';
				}
			}
		}
	}
	
	/**
	 * Maximum api version supported by the api class
	 * 
	 * @return int
	 */
	public static function __getMaximumSupportedVersion() {
		return Scope::get ( 'Restler' )->getApiVersion ();
	}
	
	/**
	 * Verifies that the requesting user is allowed to view the docs for this API
	 *
	 * @param
	 *        	$route
	 *        	
	 * @return boolean True if the user should be able to view this API's docs
	 */
	protected function verifyAccess($route) {
		if ($route ['accessLevel'] < 2) {
			return true;
		}
		if (static::$hideProtected && ! $this->_authenticated && $route ['accessLevel'] > 1) {
			return false;
		}
		if ($this->_authenticated && static::$accessControlFunction && (! call_user_func ( static::$accessControlFunction, $route ['metadata'] ))) {
			return false;
		}
		return true;
	}
}
